package requestshandler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import users.Person;
import util.ListModelator;
import util.Pair;
import books.BookInfo;
import books.Category;
import books.Loan;
import exceptions.PersonDoesNotHaveLoansException;

/**
 * This class stores all the loans, current and finished ones.
 */
public class History {

    // ---------INSTANCE VARIABLES-----------------------------------------//

    // CONVENTION: FIRST=ACTUAL LOANS // SECOND=FINISHED LOANS
    private Map<Person, Pair<List<Loan>, List<Loan>>> loansByPerson;

    private Map<Person, HashMap<Category, Integer>> loanedCategoriesCount;

    // ---------CONSTRUCTORS------------------------------------------------//

    public History() {
        loansByPerson = new HashMap<Person, Pair<List<Loan>, List<Loan>>>();
        loanedCategoriesCount = new HashMap<Person, HashMap<Category, Integer>>();
    }

    // ---------ACCESSORS---------------------------------------------------//

    public void setLoanedCategoriesCount(final Map<Person, HashMap<Category, Integer>> loanedCategoriesCount) {
        this.loanedCategoriesCount = loanedCategoriesCount;
    }

    public Map<Person, Pair<List<Loan>, List<Loan>>> getLoansByPerson() {
        return loansByPerson;
    }

    public void setLoansByPerson(final Map<Person, Pair<List<Loan>, List<Loan>>> loansByPerson) {
        this.loansByPerson = loansByPerson;
    }

    public List<Loan> getCurrentLoans(final Person aPerson) {

        if (!loansByPerson.containsKey(aPerson)) {
            validateEntryOf(aPerson);
        }
        return loansByPerson.get(aPerson).getFirst();
    }

    public List<Loan> getFinishedLoans(final Person aPerson) {

        if (!loansByPerson.containsKey(aPerson)) {
            validateEntryOf(aPerson);
        }
        return loansByPerson.get(aPerson).getSecond();
    }

    public Map<Person, HashMap<Category, Integer>> getLoanedCategoriesCount() {
        return loanedCategoriesCount;
    }

    // ---------OTHER BEHAVIOUR---------------------------------------------//

    private void validateEntryOf(final Person aPerson) {
        if (!loansByPerson.containsKey(aPerson)) {
            // If this Person does not have performed Loans, initialize the
            // loans list
            loansByPerson.put(aPerson, new Pair<List<Loan>, List<Loan>>(new ArrayList<Loan>(), new ArrayList<Loan>()));
        }
    }

    // --------//

    public void storeNewLoan(final Loan aLoan) {
        validateEntryOf(aLoan.getLoaner());
        loansByPerson.get(aLoan.getLoaner()).getFirst().add(aLoan);

        updateLoanedCategoriesCount(aLoan);
    }

    // --------//

    private void updateLoanedCategoriesCount(final Loan aLoan) {
        // Categories I am going to add
        ArrayList<Category> categoriesToAdd = (ArrayList<Category>) aLoan.getLoanedBook().getInfo().getCategories();

        if (loanedCategoriesCount.containsKey(aLoan.getLoaner())) { // New entry
                                                                    // case
            for (Category it : categoriesToAdd) {
                addTokenToCategoryInExistingPerson(aLoan.getLoaner(), it);
            }
        } else {

            // Initialize map of entried Person
            loanedCategoriesCount.put(aLoan.getLoaner(), new HashMap<Category, Integer>());

            for (Category it : categoriesToAdd) {
                loanedCategoriesCount.get(aLoan.getLoaner()).put(it, 1);
            }
        }

    }

    // ----------//

    protected void addTokenToCategoryInExistingPerson(final Person loaner, final Category cat) {
        if (loanedCategoriesCount.get(loaner).containsKey(cat)) {
            loanedCategoriesCount.get(loaner).put(cat, loanedCategoriesCount.get(loaner).get(cat) + 1);
        } else {
            loanedCategoriesCount.get(loaner).put(cat, 1);

        }

    }

    // --------//

    public void setCurrentLoanAsFinished(final Loan aLoan) {
        if (!getCurrentLoans(aLoan.getLoaner()).contains(aLoan)) {
            throw new PersonDoesNotHaveLoansException("Specified Person has not current book loans, cannot update");
        }

        getCurrentLoans(aLoan.getLoaner()).remove(aLoan);
        getFinishedLoans(aLoan.getLoaner()).add(aLoan);
        aLoan.finalizeLoan();
    }

    // --------//

    public Category getMostLoanedCategoryOf(final Person aPerson) {
        Category ret = null;
        int aux = 0;
        Iterator<Entry<Category, Integer>> it = loanedCategoriesCount.get(aPerson).entrySet().iterator();

        while (it.hasNext()) {
            Map.Entry<Category, Integer> actual = it.next();
            if (actual.getValue() > aux) {
                ret = actual.getKey();
                aux = actual.getValue();
            }
        }
        return ret;
    }

    // --------//

    public List<Loan> getActualLoans() {
        List<Loan> ret = new ArrayList<Loan>();

        for (Entry<Person, Pair<List<Loan>, List<Loan>>> it : loansByPerson.entrySet()) {
            ret.addAll(it.getValue().getFirst());
        }

        return ret;
    }

    // --------//

    public List<BookInfo> usersWhoLoanedThisBookAlsoLoaned(final BookInfo aBookInfo) {
        List<BookInfo> ret = new ArrayList<BookInfo>();

        for (Person it : loansByPerson.keySet()) {
            if (userLoanedThisBook(it, aBookInfo)) {
                ret.addAll(alsoLoaned(it, aBookInfo));
            }
        }

        return ret;
    }

    public List<BookInfo> usersWhoLoanedThisBookAlsoLoanedSpecific(final BookInfo aBookInfo) {
        List<BookInfo> ret = usersWhoLoanedThisBookAlsoLoaned(aBookInfo);
        ListModelator.filterListByCategory(ret, aBookInfo.getCategories().get(0));
        return ret;
    }

    // AUX
    public boolean userLoanedThisBook(final Person aPerson, final BookInfo aBookInfo) {
        boolean ret = false;

        for (Loan it : getFinishedLoans(aPerson)) {
            if (it.getBookInfo() == aBookInfo) {
                ret = true;
            }
        }

        return ret;
    }

    // AUX
    public List<BookInfo> alsoLoaned(final Person aPerson, final BookInfo aBookInfo) {
        List<BookInfo> ret = new ArrayList<BookInfo>();

        for (Loan it : getFinishedLoans(aPerson)) {
            if (it.getBookInfo() != aBookInfo) {
                ret.add(it.getBookInfo());
            }
        }

        return ret;
    }

}
